/******************************************************************************/
/* Mednafen Sega Saturn Emulation Module                                      */
/******************************************************************************/
/* scu.inc - SCU Emulation
**  Copyright (C) 2015-2016 Mednafen Team
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation, Inc.,
** 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

// TODO: Investigate different instruction dispatch mechanisms for DSP to take advantage of modern branch
//	 prediction, to improve performance and reduce instruction cache footprint.
// TODO: Test slave SH-2 IRQ handling.
// TODO: Open bus, and correct propagation of open bus from one bus to another.
// TODO: Consider logging DMA status register reads.
// TODO: Test indirect DMA table alignment requirements.
// TODO: Indirect same-bus DMA has kind of weird effects that might actually be useful(and hence used), so test!

/*
 Notes(assuming NTSC, HRES=0, VRES=0, LSMD=0):
   Timer0 notes:
	Counter is effectively forced to 0 while timer enable bit is 0, but it won't generate an IRQ if T0C is also 0(unless timer enable bit is set
	to 1 again)...

	Hrm...T0C of 0-263 causes interrupts.  In interlace mode though, 263 causes an interrupt at half the rate...
   Timer1 notes:
	T1S of 1-426(HRES=0x00) causes interrupt for each line, 0 and 427+ causes it every other line?


 The effect of writes to MCIEB on the interrupt output signal(as examined indirectly via IST) from the SCSP appears to be delayed somewhat;
 is the SCU buffering writes, or is the SCSP, or is it something else entirely?

 Reads from A-bus and B-bus are treated by the SCU as always 32-bit, regardless of the actual size(writes are handled properly, though).

 SCU DMA read from VRAM is reportedly unreliable?

 DMA speed for accesses to A-/B-bus, with the exceptions of A-bus CS2, should be best-case in this code
 (writes often take longer on the Saturn if the DMA write address doesn't increment).
*/

#include "scu_dsp_common.inc"

static void DSP_Reset(bool powering_up);

enum { DMA_UpdateTimingGran = 127 };

enum { DSP_UpdateTimingGran = 64 };	// Probably should keep it a multiple of 2.

struct DMAWriteTabS
{
 int16 write_addr_delta;
 uint8 write_size;
 uint8 compare;
};

static const DMAWriteTabS dma_write_tab[2/*bus*/][8/*add setting*/][4/*write align*/][12/*count*/][5] =
{
 {
  #include "scu_actab.inc"
 },
 {
  #include "scu_btab.inc"
 }
};

static const DMAWriteTabS dma_write_tab_aciv1[4][24][8] =
{
 #include "scu_aciv1tab.inc"
};

static struct DMALevelS
{
 uint32 StartReadAddr;
 uint32 StartWriteAddr;
 uint32 StartByteCount;

 bool ReadAdd;
 uint8 WriteAdd;

 bool Enable;
 int8 Active; // -1, 0, 1
 bool GoGoGadget;

 bool Indirect;
 bool ReadUpdate;
 bool WriteUpdate;
 uint8 SF;

 sscpu_timestamp_t FinishTime;

 //
 //
 //
 uint32 (*ReadFunc)(uint32 offset);
 uint32 WriteBus;
 //
 uint32 CurReadBase;
 uint32 CurReadSub;

 uint32 CurWriteAddr;
 uint32 CurByteCount;

 uint64 Buffer;
 //
 const DMAWriteTabS* WATable;
 //
 uint32 (*TableReadFunc)(uint32 offset);	// Also serves as a kind of "CurIndirect" cache of "Indirect" variable.
 uint32 CurTableAddr;
 bool FinalTransfer;
} DMALevel[3];

static sscpu_timestamp_t SCU_DMA_TimeCounter;
static sscpu_timestamp_t SCU_DMA_RunUntil;
static int32 SCU_DMA_ReadOverhead;	// range -whatever to 0.

static uint32 SCU_DMA_VDP1WriteIgnoreKludge;

static void RecalcDMAHalt(void);
static void CheckDMAStart(DMALevelS* d);
static void CheckDMASFByInt(unsigned int_which);
static INLINE void CheckForceDMAFinish(void);

static uint32 IAsserted;
static uint32 IPending;
static uint32 IMask;

static uint32 ABusIProhibit;
static bool RSEL;

static uint8 ILevel, IVec;

static INLINE void RecalcMasterIntOut(void)
{
 if(ILevel == 0)
 {
  static const uint8 internal_tab[16 + 1] =
  {
   0xF, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8,
   0x8, 0x6, 0x6, 0x5, 0x3, 0x2, 0x0, 0x0,
   0x0
  };

  static const uint8 external_tab[16 + 1]
  {
   0x7, 0x7, 0x7, 0x7, 0x4, 0x4, 0x4, 0x4,
   0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1,
   0x0
  };
  const uint32 ipmd = IPending &~ (int16)IMask;
  unsigned wi = MDFN_tzcount16(ipmd & 0xFFFF);
  unsigned we = MDFN_tzcount16(ipmd >> 16);
  unsigned olev, ovec, bpos;

  olev = internal_tab[wi];
  ovec = 0x40 + wi;
  bpos = wi;

  if(external_tab[we] > internal_tab[wi])
  {
   olev = external_tab[we];
   ovec = 0x50 + we;
   bpos = 16 + we;
  }

  if(olev != 0)
  {
   ILevel = olev;
   IVec = ovec;
   IPending &= ~(1U << bpos);
   //SS_DBGTI(SS_DBG_ERROR, "[SCU] Interrupt level=0x%02x, vector=0x%02x --- IPending=0x%04x", ILevel, IVec, IPending);
  }
 }

 CPU[0].SetIRL(ILevel);
}

static int32 Timer0_Counter;
static int32 Timer0_Compare;
static bool Timer0_Met;

static int32 Timer1_Reload;
static int32 Timer1_Counter;
static bool Timer1_Mode;
static bool Timer1_Met;

static bool Timer_Enable;

static bool HB_FromVDP2, VB_FromVDP2;

static uint8 SCU_MSH2VectorFetch(void)
{
 uint8 ret = IVec;

 //SS_DBGTI(SS_DBG_ERROR, "[SCU]   Interrupt cleared.");

 if(MDFN_UNLIKELY(ILevel == 0))
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] [MSH2] [BUG] SCU_MSH2VectorFetch() called when ILevel == 0\n");
 }

 if(MDFN_UNLIKELY(IVec == 0x40 /* || IVec == 0x41 */))	// VB In, apply cheats.
  MDFNMP_ApplyPeriodicCheats();

 IMask = 0xBFFF;

 ILevel = 0;
 RecalcMasterIntOut();

 return ret;
}

static uint8 SCU_SSH2VectorFetch(void)
{
 if(VB_FromVDP2)
  return 0x43;

 return 0x41;
// return 0xFF;	// FIXME?
}

static INLINE void ABusIRQCheck(void)
{
 const uint32 tt = (ABusIProhibit ^ IAsserted) & (IAsserted & ~0xFFFF);

 IPending |= tt;
 ABusIProhibit |= IAsserted & ~0xFFFF;

 if(tt)
  RecalcMasterIntOut();
}

static INLINE void SetInt(unsigned which, bool active)
{
 const uint32 old_IAsserted = IAsserted;

 IAsserted &= ~(1U << which);
 IAsserted |= (unsigned)active << which;

 if(which >= 16)
  ABusIRQCheck();
 else
 {
  if((old_IAsserted ^ IAsserted) & IAsserted)
  {
   IPending |= 1U << which;
   CheckDMASFByInt(which);
   RecalcMasterIntOut();
  }
 }
}

void SCU_SetInt(unsigned which, bool active)
{
 SetInt(which, active);
}

static INLINE void Timer0_Check(void)
{
 if(Timer_Enable)
 {
  Timer0_Met = (Timer0_Counter == Timer0_Compare);
  SetInt(SCU_INT_TIMER0, Timer0_Met);
 }
}

static INLINE void Timer1_Check(void)
{
 if(Timer_Enable)
 {
  Timer1_Met |= (Timer1_Counter == 0 && (!Timer1_Mode || Timer0_Met));
  SetInt(SCU_INT_TIMER1, Timer1_Met);
 }
}

int32 SCU_SetHBVB(int32 pclocks, bool new_HB_FromVDP2, bool new_VB_FromVDP2)
{
 const bool HB_Start = (HB_FromVDP2 ^ new_HB_FromVDP2) & new_HB_FromVDP2;
 const bool VB_End   = (VB_FromVDP2 ^ new_VB_FromVDP2) & VB_FromVDP2;

 if(Timer_Enable)
 {
  if(VB_End)
   Timer0_Counter = 0;

  if(HB_Start)
   Timer0_Counter = (Timer0_Counter + 1) & 0x1FF;

  Timer0_Check();

  if(pclocks > 0)
  {
   Timer1_Counter = (Timer1_Counter - pclocks) & 0x1FF;
   Timer1_Check();
  }

  if(Timer1_Met && HB_Start)
  {
   Timer1_Met = false;
   Timer1_Counter = Timer1_Reload;

   SetInt(SCU_INT_TIMER1, Timer1_Met);
  }
 }

 SetInt(SCU_INT_HBIN, new_HB_FromVDP2);
 SetInt(SCU_INT_VBIN, new_VB_FromVDP2);
 SetInt(SCU_INT_VBOUT, !new_VB_FromVDP2);

 //
 //
 CPU[1].SetIRL(((new_VB_FromVDP2 | new_HB_FromVDP2) << 1) | (new_VB_FromVDP2 << 2));
 //
 //
 //
 HB_FromVDP2 = new_HB_FromVDP2;
 VB_FromVDP2 = new_VB_FromVDP2;

 return Timer1_Counter ? Timer1_Counter : 0x200;
}

static void SCU_Init(void)
{
 SCU_DMA_TimeCounter = 0;
 SCU_DMA_RunUntil = 0;
 IAsserted = 0;
 HB_FromVDP2 = false;
 VB_FromVDP2 = false;

 DSP_Init();
}

void SCU_Reset(bool powering_up)
{
 ILevel = IVec = 0;
 IMask = 0xBFFF;
 IPending = 0;
 ABusIProhibit = 0;
 RSEL = 0;

 if(powering_up)
  memset(DMALevel, 0x00, sizeof(DMALevel));

 for(auto& d : DMALevel)
 {
  d.ReadAdd = true;
  d.WriteAdd = 0x1;

  d.Enable = false;
  d.GoGoGadget = false;
  d.Active = false;

  d.Indirect = false;
  d.ReadUpdate = false;
  d.WriteUpdate = false;
  d.SF = 0;
 }
 //SCU_DMA_CycleCounter = 0;
 SCU_DMA_ReadOverhead = 0;

 SCU_DMA_VDP1WriteIgnoreKludge = 0;

 RecalcDMAHalt();

 DSP_Reset(powering_up);

 RecalcMasterIntOut();
}

static void SCU_AdjustTS(const int32 delta)
{
 SCU_DMA_TimeCounter += delta;
 SCU_DMA_RunUntil += delta;
 for(auto& d : DMALevel)
 {
  if(d.Active < 0)
   d.FinishTime += delta;
 }

 //
 //
 //
 if(DSP.T0_Until > 0x10000000)
  DSP.T0_Until = 0x10000000;

 DSP.LastTS += delta;
 if(DSP.LastTS < 0)
 {
  // TODO: Fix properly.
  //printf("%d\n", DSP.LastTS);
  DSP.LastTS = 0;
 }
}

//
// TODO: Test to see if the entire data bus or only parts are asserted for uint8 and uint16 reads
//
template<typename T, bool IsWrite>
static INLINE void SCU_RegRW_DB(uint32 A, uint32* DB)
{
 unsigned mask;

 switch(sizeof(T))
 {
  case 1: mask = 0xFF << (((A & 3) ^ 3) << 3); break;
  case 2: mask = 0xFFFF << (((A & 2) ^ 2) << 3); break;
  case 4: mask = 0xFFFFFFFF; break;
 }

 if(IsWrite)
 {
  SS_DBGTI(SS_DBG_SCU_REGW, "[SCU] %zu-byte write to 0x%02x, DB=0x%08x", sizeof(T), A & 0xFC, *DB);

  switch(A & 0xFC)
  {
   default:
	SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Unknown %zu-byte write to 0x%08x(DB=0x%08x).\n", sizeof(T), A, *DB);
	break;

   case 0x90:	// Timer 0 Compare
	{
	 VDP2::Update(SH7095_mem_timestamp);
	 Timer0_Compare = (Timer0_Compare &~ mask) | (*DB & mask & 0x3FF);
	 SS_SetEventNT(&events[SS_EVENT_VDP2], VDP2::Update(SH7095_mem_timestamp));
	}
	break;

   case 0x94:	// Timer 1 Reload Value
	{
	 VDP2::Update(SH7095_mem_timestamp);
	 Timer1_Reload = (Timer1_Reload &~ mask) | (*DB & mask & 0x1FF);
	 SS_SetEventNT(&events[SS_EVENT_VDP2], VDP2::Update(SH7095_mem_timestamp));
	}
	break;

   case 0x98:	// Timer Control
	{
	 VDP2::Update(SH7095_mem_timestamp);
	 uint32 tmp = (Timer1_Mode << 8) | (Timer_Enable << 0);
	 tmp = (tmp &~ mask) | (*DB & mask);
	 Timer1_Mode = (tmp >> 8) & 1;
	 Timer_Enable = (tmp >> 0) & 1;

	 if(!Timer_Enable)
	 {
	  Timer0_Counter = 0;
	 }

	 SS_SetEventNT(&events[SS_EVENT_VDP2], VDP2::Update(SH7095_mem_timestamp));
	}
	break;

   case 0xA0:
	IMask = (IMask &~ mask) | (*DB & mask & 0xBFFF);
	RecalcMasterIntOut();
	break;

   case 0xA4:
	IPending &= *DB | ~mask;
	RecalcMasterIntOut();
	break;

   case 0xA8:
	if(*DB & mask & 0x0001)
	{
	 ABusIProhibit = 0; //&= ~IAsserted;
	 ABusIRQCheck();
	}
	break;

   case 0xC4:
	RSEL = (RSEL &~ mask) | (*DB & mask & 0x1);
	if(MDFN_UNLIKELY(!RSEL))
	{
	 SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Write to RSEL, RSEL=0\n");
	}
	break;

   case 0x00:
   case 0x20:
   case 0x40:
	{
	 auto& d = DMALevel[(A >> 5) & 0x3];

	 d.StartReadAddr = (d.StartReadAddr &~ mask) | (*DB & mask & 0x07FFFFFF);
	}
	break;

   case 0x04:
   case 0x24:
   case 0x44:
	{
	 auto& d = DMALevel[(A >> 5) & 0x3];

	 d.StartWriteAddr = (d.StartWriteAddr &~ mask) | (*DB & mask & 0x07FFFFFF);
	}
	break;

   case 0x08:
   case 0x28:
   case 0x48:
	{
	 const unsigned level = (A >> 5) & 0x3;
	 auto& d = DMALevel[level];

	 d.StartByteCount = (d.StartByteCount &~ mask) | (*DB & mask & (level ? 0x00000FFF : 0x000FFFFF));
        }
	break;

   case 0x0C:
   case 0x2C:
   case 0x4C:
	{
	 auto& d = DMALevel[(A >> 5) & 0x3];
	 uint32 tmp = (d.ReadAdd << 8) | (d.WriteAdd << 0);

	 tmp = (tmp &~ mask) | (*DB & mask);

	 d.ReadAdd = (tmp >> 8) & 0x1;
	 d.WriteAdd = (tmp >> 0) & 0x7;
	}
	break;

   case 0x10:
   case 0x30:
   case 0x50:
	{
	 const unsigned level = (A >> 5) & 0x3;
	 auto& d = DMALevel[level];
	 uint32 tmp = (d.Enable << 8);

	 tmp = (tmp &~ mask) | (*DB & mask);	 
	 d.Enable = (tmp >> 8) & 0x1;

	 if((tmp & 0x1) && d.Enable && d.SF == 0x7)
	 {
	  SCU_UpdateDMA(SH7095_mem_timestamp);

	  d.GoGoGadget = true;
	  CheckDMAStart(&d);

	  SS_SetEventNT(&events[SS_EVENT_SCU_DMA], SCU_UpdateDMA(SH7095_mem_timestamp));
	 }
	}
	break;

   case 0x14:
   case 0x34:
   case 0x54:
	{
	 auto& d = DMALevel[(A >> 5) & 0x3];
	 uint32 tmp = (d.Indirect << 24) | (d.ReadUpdate << 16) | (d.WriteUpdate << 8) | (d.SF << 0);

	 tmp = (tmp &~ mask) | (*DB & mask);

	 d.Indirect = (tmp >> 24) & 0x1;
	 d.ReadUpdate = (tmp >> 16) & 0x1;
	 d.WriteUpdate = (tmp >> 8) & 0x1;
	 d.SF = (tmp >> 0) & 0x7;
	}
	break;

   case 0x60:
	// TODO: Test
	if(*DB & mask & 0x1)
	{
	 SCU_DMA_ReadOverhead = 0;
	 for(unsigned level = 0; level < 3; level++)
	 {
	  auto& d = DMALevel[level];

	  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Forced stop of DMA level %u\n", level);

	  d.Active = false;
	  d.GoGoGadget = false;
	 }
	 RecalcDMAHalt();
	}
	break;

   case 0x80:
	SCU_UpdateDSP(SH7095_mem_timestamp);

	if(*DB & (1U << 25))	// Pause start
	 DSP.State |= DSPS::STATE_MASK_PAUSE;
	else if(*DB & (1U << 26))	// Pause stop
	 DSP.State &= ~DSPS::STATE_MASK_PAUSE;
	else
	{
	 if(*DB & (1U << 16))	// Execute
	  DSP.State |= DSPS::STATE_MASK_EXECUTE;
	 else if(!(*DB & (1U << 16)))	// Execute stop
	 {
	  if(DSP.State & DSPS::STATE_MASK_EXECUTE)
	  {
	   DSP.NextInstr = DSP_DecodeInstruction(0);
	   DSP.State &= ~DSPS::STATE_MASK_EXECUTE;

	   if(DSP.CycleCounter < 0)
	    DSP.CycleCounter = 0;

	   if(DSP.T0_Until < 0)
	    DSP.T0_Until = 0;
	  }
	 }

	 if(MDFN_UNLIKELY(*DB & (1U << 17)))	// Step
	 {
	  if(DSP.State == 0)
	  {
	   ((void (*)(void))(DSP_INSTR_BASE_UIPT + (uintptr_t)(DSP_INSTR_RECOVER_TCAST)DSP.NextInstr))();
	   if(DSP.CycleCounter < -(DSP_EndCCSubVal / 2))	// Ugh
	    DSP.CycleCounter += DSP_EndCCSubVal;
	  }
	 }
        }

	if(*DB & (1U << 15))	// PC load
	 DSP.PC = *DB;

	SS_SetEventNT(&events[SS_EVENT_SCU_DSP], (DSP.IsRunning() ? SH7095_mem_timestamp + (DSP_UpdateTimingGran / 2) : SS_EVENT_DISABLED_TS));
	break;

   case 0x84:
	if(!DSP.IsRunning())
	 DSP.ProgRAM[DSP.PC++] = DSP_DecodeInstruction(*DB);
	break;

   case 0x88:
	DSP.RA = *DB;
	break;

   case 0x8C:
	if(!DSP.IsRunning())
	 (&DSP.DataRAM[0][0])[DSP.RA++] = *DB;
	break;
  }
 }
 else
 {
  switch(A & 0xFC)
  {
   default:
	SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Unknown %zu-byte read from 0x%08x.\n", sizeof(T), A);
	*DB = 0;
	break;

   case 0xA4:
	*DB = IPending;
	break;

   case 0xA8:
	*DB = 0; //ABusIAck;
	break;

   case 0xC4:
	*DB = RSEL;
	break;

   case 0xC8:
	*DB = 0x4;
	break;
   //
   //
   //
   case 0x00:
   case 0x20:
   case 0x40:
	{
	 auto const& d = DMALevel[(A >> 5) & 0x3];

	 *DB = d.StartReadAddr;
	}
	break;

   case 0x04:
   case 0x24:
   case 0x44:
	{
	 auto const& d = DMALevel[(A >> 5) & 0x3];

	 *DB = d.StartWriteAddr;
	}
	break;

   case 0x7C:
	{
	 uint32 tmp = 0;

	 for(unsigned level = 0; level < 3; level++)
	 {
	  auto& d = DMALevel[level];

	  if(d.Active)
	  {
	   tmp |= 0x10 << (level << 2);
	  }
	 }

	 if(DMALevel[0].Active && (DMALevel[1].Active || DMALevel[2].Active))
	  tmp |= 1U << 16;

	 if(DMALevel[1].Active && DMALevel[2].Active)
	  tmp |= 1U << 17;

	 *DB = tmp;
	}
	break;

   case 0x80:
	SS_SetEventNT(&events[SS_EVENT_SCU_DSP], SCU_UpdateDSP(SH7095_mem_timestamp));	// TODO: Remove?
	{
	 uint32 tmp;

	 tmp = DSP.PC;
	 tmp |= (DSP.T0_Until < DSP.CycleCounter) << 23;
	 tmp |= DSP.FlagS << 22;
	 tmp |= DSP.FlagZ << 21;
	 tmp |= DSP.FlagC << 20;
	 tmp |= DSP.FlagV << 19;
	 tmp |= DSP.FlagEnd << 18;
	 tmp |= DSP.IsRunning() << 16;
	 *DB = tmp;
	 //
	 DSP.FlagV = false;
	 DSP.FlagEnd = false;
	 SCU_SetInt(SCU_INT_DSP, false);
	}
	break;

   case 0x8C:
	if(!DSP.IsRunning())
	 *DB = (&DSP.DataRAM[0][0])[DSP.RA++];
	else
	 *DB = 0xFFFFFFFF;
	break;
  }
 }
}


template<typename T, bool IsWrite, bool SH32 = false>
static INLINE void BBusRW_DB(uint32 A, uint16* DB, int32* time_thing, int32* dma_time_thing = NULL, int32* sh2_dma_time_thing = NULL)	// add to time_thing, subtract from dma_time_thing
{
 static_assert(IsWrite || sizeof(T) == 2, "Wrong type.");

 //
 // VDP1
 //
 if(A >= 0x05C00000 && A <= 0x05D7FFFF)
 {
  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= IsWrite ? (SH32 ? 0 : 6) : 10;

  if(dma_time_thing != NULL)
  {
   *dma_time_thing -= 1;

   if(IsWrite)
   {
    if(MDFN_UNLIKELY(A >= 0x05D00000))
    {
     const bool ignore_write = (A >= 0x5D00004 && SCU_DMA_VDP1WriteIgnoreKludge > 0) | (SCU_DMA_VDP1WriteIgnoreKludge & 0x1);

     SCU_DMA_VDP1WriteIgnoreKludge++;
     if(ignore_write)
      return;
     //printf("%08x %04x\n", A, *DB);
    }
    else
     SCU_DMA_VDP1WriteIgnoreKludge = 0;
   }
  }

  if(time_thing != NULL)
  {
   if(IsWrite)
    *time_thing += SH32 ? 0 : 11;
   else
    *time_thing += 14;

   CheckEventsByMemTS();
  }

  if(IsWrite)
  {
   if(sizeof(T) == 1)
    VDP1::Write8_DB(A, *DB);
   else
    VDP1::Write16_DB(A, *DB);
  }
  else
  {
   *DB = VDP1::Read16_DB(A);
  }

  return;
 }

 //
 // VDP2
 //
 if(A >= 0x05E00000 && A <= 0x05FBFFFF)
 {
  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= IsWrite ? (SH32 ? 0 : 5) : 10;

  if(dma_time_thing != NULL)
  {
   *dma_time_thing -= 1;
  }

  if(time_thing != NULL)
  {
   if(IsWrite)
    *time_thing += SH32 ? 0 : 5;
   else
    *time_thing += 20;

   CheckEventsByMemTS();
  }

  if(IsWrite)
  {
   uint32 expenalty;

   if(sizeof(T) == 1)
    expenalty = VDP2::Write8_DB(A, *DB);
   else
    expenalty = VDP2::Write16_DB(A, *DB);

   if(dma_time_thing != NULL)
   {
    //if(expenalty)
    // printf("%u\n", expenalty);

    *dma_time_thing -= expenalty;
   }
  }
  else
  {
   *DB = VDP2::Read16_DB(A);
  }

  return;
 }

 //
 // SCSP
 // 
 if(A >= 0x05A00000 && A <= 0x05BFFFFF)
 {
  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= 13;

  if(dma_time_thing != NULL)
  {
   *dma_time_thing -= 13;
  }

  if(time_thing != NULL)
  {
   if(IsWrite)
    *time_thing += SH32 ? 13 : 19;
   else
    *time_thing += 24;
  }

  if(IsWrite)
  {
   if(sizeof(T) == 1)
    SOUND_Write8(A & 0x1FFFFF, *DB >> (((A & 1) ^ 1) << 3));
   else
    SOUND_Write16(A & 0x1FFFFF, *DB);
  }
  else
   *DB = SOUND_Read16(A & 0x1FFFFF); 

  return;
 }
 //
 //
 //
 if(sh2_dma_time_thing != NULL)
  *sh2_dma_time_thing -= 1;

 if(dma_time_thing != NULL)
  *dma_time_thing -= 1;

 if(IsWrite)
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[B-Bus] Unknown %zu-byte write of 0x%08x(DB=0x%04x)\n", sizeof(T), A, *DB);
 else
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[B-Bus] Unknown %zu-byte read from 0x%08x\n", sizeof(T), A);

  *DB = 0;
 }
}

template<typename T, bool IsWrite, bool SH32 = false>
static INLINE void ABusRW_DB(uint32 A, uint16* DB, int32* time_thing, int32* dma_time_thing = NULL, int32* sh2_dma_time_thing = NULL)	// add to time_thing, subtract from dma_time_thing
{
 //
 // A-Bus CS0 and CS1
 //
 if(A >= 0x02000000 && A <= 0x04FFFFFF)
 {
  // [(bool)(A & 0x04000000)]

  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= 1;	// TODO

  if(dma_time_thing != NULL)
   *dma_time_thing -= 1;	// TODO

  if(IsWrite)
  {
   if(sizeof(T) == 1)
    CART_CS01_Write8_DB(A, DB);
   else
    CART_CS01_Write16_DB(A, DB);
  }
  else
   CART_CS01_Read16_DB(A, DB);

  return;
 }

 //
 // A-bus Dummy
 //
 if(MDFN_UNLIKELY(A >= 0x05000000 && A <= 0x057FFFFF))
 {
  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= 16;

  if(dma_time_thing != NULL)
  {
   *dma_time_thing -= 16;
  }

  if(IsWrite)
   SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[A-Bus CSD] Unknown %zu-byte write to 0x%08x(DB=0x%04x)\n", sizeof(T), A, *DB);
  else
   SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[A-Bus CSD] Unknown %zu-byte read from 0x%08x\n", sizeof(T), A);

  return;
 }


 //
 // A-Bus CS2
 //
 if(A >= 0x05800000 && A <= 0x058FFFFF)
 {
  if(sh2_dma_time_thing != NULL)
   *sh2_dma_time_thing -= 8;

  if(dma_time_thing != NULL)
  {
   *dma_time_thing -= 8;
  }

  if(time_thing)
  {
   if(IsWrite)
    *time_thing += 8;
   else
    *time_thing += 8;
  }

  if((A & 0x7FFF) < 0x1000)
  {
   const uint32 offset = (A & 0x3F) >> 2;
   const uint32 mask = (sizeof(T) == 2) ? 0xFFFF : (0xFF << (((A & 1) ^ 1) << 3));

   if(IsWrite)
   {
    CDB_Write_DBM(offset, *DB, mask);
   }
   else
   {
    if(!SH32 || !(A & 0x80000)) // CD block seems to effectively ignore second read access in 32-bit reads somehow, tested to occur HIRQ and the FIFO at least...
     *DB = CDB_Read(offset);
   }
   return;
  }

  if(IsWrite)
  {
   if(sizeof(T) == 1)
    CART_CS2_Write8_DB(A, DB);
   else
    CART_CS2_Write16_DB(A, DB);
  }
  else
   CART_CS2_Read16_DB(A, DB);

  return;
 }

 if(sh2_dma_time_thing != NULL)
  *sh2_dma_time_thing -= 1;

 if(dma_time_thing != NULL)
  *dma_time_thing -= 1;

 if(IsWrite)
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[A-Bus] Unknown %zu-byte write to 0x%08x(DB=0x%04x)\n", sizeof(T), A, *DB);
 else
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[A-Bus] Unknown %zu-byte read from 0x%08x\n", sizeof(T), A);
}

template<typename T>
static INLINE void ABus_Write_DB32(uint32 A, uint32 DB32, int32* time_thing, int32* dma_time_thing = NULL, int32* sh2_dma_time_thing = NULL)
{
 if(sizeof(T) == 4)
 {
  uint16 tmp;

  tmp = DB32 >> 16;
  ABusRW_DB<uint16, true>(A, &tmp, time_thing, dma_time_thing, sh2_dma_time_thing);

  tmp = DB32 >> 0;
  ABusRW_DB<uint16, true, true>(A | 2, &tmp, time_thing, dma_time_thing, sh2_dma_time_thing);
 }
 else
 {
  uint16 tmp = DB32 >> (((A & 2) ^ 2) << 3);

  ABusRW_DB<T, true>(A, &tmp, time_thing, dma_time_thing, sh2_dma_time_thing);
 }
}

// Lower 2 bits of A should be 0
static INLINE uint32 ABus_Read(uint32 A, int32* time_thing, int32* dma_time_thing = NULL, int32* sh2_dma_time_thing = NULL)
{
 uint32 ret;
 uint16 tmp = 0xFFFF;

 ABusRW_DB<uint16, false>(A, &tmp, time_thing, dma_time_thing, sh2_dma_time_thing);
 ret = tmp << 16;

 ABusRW_DB<uint16, false, true>(A | 2, &tmp, time_thing, dma_time_thing, sh2_dma_time_thing);
 ret |= tmp << 0;

 return ret;
}

template<typename T, bool IsWrite>
static INLINE void SCU_FromSH2_BusRW_DB(uint32 A, uint32* DB, int32* SH2DMAHax)
{
 //
 // A bus
 //
 if(A >= 0x02000000 && A <= 0x058FFFFF)
 {
  CheckForceDMAFinish();

  if(IsWrite)
   ABus_Write_DB32<T>(A, *DB, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);
  else // A-bus reads are always 32-bit(divided into two 16-bit accesses internally)
   *DB = ABus_Read(A &~ 0x3, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);

  return;
 }


 //
 // B bus
 //
 if(A >= 0x05A00000 && A <= 0x05FBFFFF)
 {
  CheckForceDMAFinish();

  if(IsWrite)
  {
   if(sizeof(T) == 4)
   {
    uint16 tmp;

    tmp = *DB >> 16;
    BBusRW_DB<uint16, true>(A, &tmp, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);

    tmp = *DB >> 0;
    BBusRW_DB<uint16, true, true>(A | 2, &tmp, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);
   }
   else
   {
    uint16 tmp = *DB >> (((A & 2) ^ 2) << 3);

    BBusRW_DB<T, true>(A, &tmp, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);
   }
  }
  else // B-bus reads are always 32-bit(divided into two 16-bit accesses internally)
  {
   uint16 tmp = 0;

   BBusRW_DB<uint16, false>(A, &tmp, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);
   *DB = tmp << 16;

   BBusRW_DB<uint16, false, true>(A | 2, &tmp, SH2DMAHax ? NULL : &SH7095_mem_timestamp, NULL, SH2DMAHax);
   *DB |= tmp << 0;
  }
  return;
 }


 //
 // SCU registers
 //
 if(A >= 0x05FE0000 && A <= 0x05FEFFFF)
 {
  if(!SH2DMAHax)
  {
   SH7095_mem_timestamp += IsWrite ? 4 : 8;
   CheckEventsByMemTS();
  }
  else
   *SH2DMAHax -= IsWrite ? 4 : 8;

  SCU_RegRW_DB<T, IsWrite>(A, DB);
  return;
 }

 // TODO: (investigate 0x5A80000-0x5AFFFFF open bus region)
 //
 //if(A >= 0x05A00000 && A <= 0x05BFFFFF)
 //{
 // return 0;
 //}

 if(IsWrite)
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SH2->SCU BUS] Unknown %zu-byte write to 0x%08x(DB=0x%08x)\n", sizeof(T), A, *DB);
 else
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SH2->SCU BUS] Unknown %zu-byte read from 0x%08x\n", sizeof(T), A);

 if(!SH2DMAHax)
  SH7095_mem_timestamp += IsWrite ? 4 : 7;
 else
  *SH2DMAHax -= IsWrite ? 4 : 7;
}





//
// Offset should have lower 2 bits as 0.
//
static uint32 DMA_ReadABus(uint32 offset)
{
 return ABus_Read(offset, NULL, &SCU_DMA_ReadOverhead);
}

static uint32 DMA_ReadBBus(uint32 offset)
{
 uint32 ret;
 uint16 tmp = 0;

 BBusRW_DB<uint16, false>(offset | 0, &tmp, NULL, &SCU_DMA_ReadOverhead);
 ret = tmp << 16;

 BBusRW_DB<uint16, false, true>(offset | 2, &tmp, NULL, &SCU_DMA_ReadOverhead);
 ret |= tmp << 0;

 return ret;
}

static uint32 DMA_ReadCBus(uint32 offset)
{
 return ne16_rbo_be<uint32>(WorkRAMH, offset & 0xFFFFC);
}

static INLINE int AddressToBus(uint32 A)
{
 int ret = -1;

 if(A >= 0x02000000 && A <= 0x058FFFFF)
  ret = 0;
 else if(A >= 0x05A00000 && A <= 0x05FBFFFF)
  ret = 1;
 else if(A >= 0x06000000)
  ret = 2;

 return ret;
}

static uint32 (*const rftab[3])(uint32) = { DMA_ReadABus, DMA_ReadBBus, DMA_ReadCBus };

static bool StartDMATransfer(DMALevelS* d, const uint32 ra, const uint32 wa, const uint32 bc)
{
 int rb, wb;

 SCU_DMA_VDP1WriteIgnoreKludge = 0;

 rb = AddressToBus(ra);
 wb = AddressToBus(wa);

 SS_DBGTI(SS_DBG_SCU, "[SCU] Starting DMA level %d transfer; ra=0x%08x wa=0x%08x bc=0x%08x - read_inc=%d write_inc=0x%01x - indirect=%d %d", (int)(d - DMALevel), ra, wa, bc, d->ReadAdd, d->WriteAdd, d->Indirect, d->SF);

 if(MDFN_UNLIKELY(rb == -1))
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Attempted DMA from illegal address 0x%08x\n", ra);
  return false;
 }

 if(MDFN_UNLIKELY(wb == -1))
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Attempted DMA to illegal address 0x%08x\n", wa);
  return false;
 }

 if(MDFN_UNLIKELY(rb == wb))
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Attempted illegal same-bus DMA from 0x%08x to 0x%08x\n", ra, wa);
  return false;
 }

 //
 //
 //
 if((wa & 0x1) && wb == 1 && d->WriteAdd != 0x1)
 {
  //
  // This sort of DMA is buggy on real hardware in weird ways(like the bus state controller is getting seriously confused), which we don't emulate.
  //
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Sketchy DMA of 0x%08x bytes from 0x%08x to unaligned B-bus address 0x%08x with write add value 0x%02x\n", bc, ra, wa, d->WriteAdd);
 }

 if(wb == 0)
 {
  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Possibly sketchy DMA of 0x%08x bytes from 0x%08x to A-bus address 0x%08x\n", bc, ra, wa);
 }

 d->CurReadBase = ra &~ 0x3;
 d->CurReadSub = ra & 0x3;

 d->CurWriteAddr = wa;

 d->CurByteCount = bc;

 d->ReadFunc = rftab[rb];
 d->WriteBus = wb;

 d->Buffer = d->ReadFunc(d->CurReadBase);

 if(wb != 0x1 && d->WriteAdd == 0x1)
  d->WATable = &dma_write_tab_aciv1[wa & 0x3][(bc < 16) ? bc : (16 | (bc & 0x7))][0];
 else
  d->WATable = &dma_write_tab[wb == 1][d->WriteAdd][wa & 0x3][(bc < 12) ? bc : (8 | (bc & 0x3))][0];

 return true;
}

static bool NextIndirect(DMALevelS* d)
{
 // count, dest, src
 uint32 tmp[3];

 SS_DBG(SS_DBG_SCU, "[SCU] DMA level %d reading indirect table entries @ 0x%08x\n", (int)(d - DMALevel), d->CurTableAddr);

 for(unsigned i = 0; i < 3; i++)
 {
  tmp[i] = d->TableReadFunc(d->CurTableAddr);
  d->CurTableAddr += (d->ReadAdd ? 4 : 0);
 }

 d->FinalTransfer = (bool)(tmp[2] & 0x80000000);

 tmp[0] &= 0xFFFFF;

 if(!tmp[0])
  tmp[0] = 0x100000;

 return StartDMATransfer(d, tmp[2] & 0x07FFFFFF, tmp[1] & 0x07FFFFFF, tmp[0]);
}

bool SCU_CheckVDP1HaltKludge(void)
{
 bool ret = false;

 for(int level = 2; level >= 0; level--)
 {
  DMALevelS* d = &DMALevel[level];

  if(d->Active > 0)
  {
   if(d->WriteBus == 1 && d->ReadFunc == DMA_ReadCBus && d->CurWriteAddr >= 0x5C00000 && d->CurWriteAddr <= 0x5DFFFFF)
    ret = true;
   //else if(d->WriteBus == 2 && d->ReadFunc == DMA_ReadBBus && d->CurReadBase >= 0x5C00000 && d->CurReadBase <= 0x5C7FFFF)
   // ret = true;

   break;
  }
 }

 return ret;
}

static void RecalcDMAHalt(void)
{
 bool Halted = false;

 for(int level = 2; level >= 0; level--)
 {
  DMALevelS* d = &DMALevel[level];

  if(d->Active > 0)
  {
   if(d->WriteBus == 2 || d->ReadFunc == DMA_ReadCBus)
    Halted = true;
#if 1
   //
   // TODO: See how halting works when a higher-priority DMA A-bus<->B-bus is running at the same time as a lower priority A/B-bus<->C-bus DMA.
   //	    For now, just print a warning message if such a situation occurs.
   else
   {
    for(int sl = level - 1; sl >= 0; sl--)
    {
     DMALevelS* sld = &DMALevel[sl];
     if(sld->Active && (sld->WriteBus == 2 || sld->ReadFunc == DMA_ReadCBus))
     {
      SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Higher priority A-bus<->B-bus DMA(level %d) running while lower-priority A-/B-bus<->C-bus DMA(level %d) is pending.\n", level, sl);
     }
    }
   }
#endif
   break;
  }
 }

 //fprintf(stderr, "SCU: %d --- %d %d %d\n", Halted, DMALevel[0].Active, DMALevel[1].Active, DMALevel[2].Active);

 CPU[0].SetExtHalt(Halted);
 CPU[1].SetExtHalt(Halted);
}

// TODO: Alter write tables to use -1, 0, 1 for 1, 2, 4

static void CheckDMAStart(DMALevelS* d)
{
 if(!d->Active && d->GoGoGadget)
 {
  d->GoGoGadget = false;
  d->FinalTransfer = true;
  d->TableReadFunc = NULL;

  if(d->Indirect)
  {
   int tb;

   d->CurTableAddr = d->StartWriteAddr & 0x07FFFFFC;	// Tested, lower 2 bits are 0 on DMA end when write address update enabled.
   tb = AddressToBus(d->CurTableAddr);

   if(tb < 0)
    SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Invalid DMA indirect mode table address 0x%08x\n", d->CurTableAddr);
   else
   {
    d->TableReadFunc = rftab[tb];

    if(NextIndirect(d))
    {
     d->Active = true;
     RecalcDMAHalt();
    }
   }
  }
  else
  {
   if(!StartDMATransfer(d, d->StartReadAddr, d->StartWriteAddr, (!d->StartByteCount) ? ((d - DMALevel) ? 0x1000 : 0x100000) : d->StartByteCount))
   {
    SCU_SetInt(SCU_INT_DMA_ILL, true);
    SCU_SetInt(SCU_INT_DMA_ILL, false);
   }
   else
   {
    d->Active = true;
    RecalcDMAHalt();
   }
  }
 }
}

static void CheckDMASFByInt(unsigned int_which)
{
 static const uint8 sf_to_int_tab[7] =
 {
  SCU_INT_VBIN,	SCU_INT_VBOUT, SCU_INT_HBIN, SCU_INT_TIMER0,
  SCU_INT_TIMER1, SCU_INT_SCSP, SCU_INT_VDP1
 };

 for(unsigned level = 0; level < 3; level++)
 {
  auto& d = DMALevel[level];

  if(d.Enable && d.SF < 0x7 && sf_to_int_tab[d.SF] == int_which)
  {
   d.GoGoGadget = true;
   CheckDMAStart(&d);
  }
 }
}

template<unsigned count>
static INLINE uint32 DMA_Read(DMALevelS* d)
{
 int shift = ((0x3 ^ (d->CurReadSub & 0x3)) - (0x3 ^ (d->CurWriteAddr & 0x3 & (4 - count)))) * 8;

 //printf("Read: CurReadSub=0x%02x, CurWriteAddr=0x%08x, count=%zu --- ", d->CurReadSub, d->CurWriteAddr, count);

 d->CurReadSub += count;
 if(d->CurReadSub > 4)
 {
  if((d->CurReadSub - count) < 4)
   shift += 32;

  d->CurReadSub -= 4; //&= 0x3;
  d->CurReadBase += (d->ReadAdd ? 4 : 0);
  //
  SCU_DMA_TimeCounter -= SCU_DMA_ReadOverhead;
  SCU_DMA_ReadOverhead = 0;
  uint32 tmp = d->ReadFunc(d->CurReadBase);
  d->Buffer <<= 32;
  d->Buffer |= tmp;
 }

 //printf("buffer=%016llx, shift=%d\n", (uint64)d->Buffer, shift);

 if(shift > 0)
  return d->Buffer >> shift;
 else
  return d->Buffer << -shift;
}

template<unsigned WriteBus, typename T>
static INLINE void DMA_Write(DMALevelS* d, uint32 DB)
{
 const uint32 A = d->CurWriteAddr &~ (sizeof(T) - 1);
 int32 WriteOverhead = 0;

 //printf("Write: %zu %08x %08x\n", sizeof(T), A, DB);
 if(WriteBus == 0)
 {
  ABus_Write_DB32<T>(A, DB, NULL, &WriteOverhead);
 }
 else if(WriteBus == 1)
 {
  uint16 DB16;

  DB16 = DB >> (((A & 0x2) ^ 0x2) * 8);
  BBusRW_DB<T, true>(A, &DB16, NULL, &WriteOverhead);
 }
 else
 {
  ne16_wbo_be<T>(WorkRAMH, A & 0xFFFFF, DB >> (((A & 3) ^ (4 - sizeof(T))) << 3));
 }

 SCU_DMA_TimeCounter -= WriteOverhead;
 SCU_DMA_ReadOverhead = std::min<int32>(0, SCU_DMA_ReadOverhead - WriteOverhead);
 d->CurByteCount -= sizeof(T);
}

template<unsigned WriteBus>
static bool NO_INLINE DMA_Loop(DMALevelS* d)
{
 while(MDFN_LIKELY(d->Active > 0 && SCU_DMA_TimeCounter < SCU_DMA_RunUntil))
 {
  switch(d->WATable->write_size)
  {
   case 0x1: DMA_Write<WriteBus, uint8> (d, DMA_Read<1>(d)); break;
   case 0x2: DMA_Write<WriteBus, uint16>(d, DMA_Read<2>(d)); break;
   case 0x4: DMA_Write<WriteBus, uint32>(d, DMA_Read<4>(d)); break;
  }
  d->CurWriteAddr += d->WATable->write_addr_delta;

  if(d->CurByteCount <= (uint32)(int8)d->WATable->compare)
   d->WATable++;

  if(MDFN_UNLIKELY(!d->CurByteCount))
  {
   SCU_DMA_TimeCounter -= SCU_DMA_ReadOverhead;
   SCU_DMA_ReadOverhead = 0;
   return true;
  }
 }

 return false;
}

//
// TODO: Check start read/write address updating when wrapping to next bus or beyond end of SDRAM.
//
static INLINE void UpdateDMAInner(DMALevelS* d)
{
 static bool (*const LoopFuncs[3])(DMALevelS*) = { DMA_Loop<0>, DMA_Loop<1>, DMA_Loop<2> };

 if(MDFN_UNLIKELY(LoopFuncs[d->WriteBus](d)))
 {
  if(d->TableReadFunc && !d->FinalTransfer)
  {
   NextIndirect(d);
  }
  else
  {
   if(d->ReadUpdate && !d->TableReadFunc)
      d->StartReadAddr = (d->CurReadBase + d->CurReadSub) & 0x07FFFFFF;

   if(d->WriteUpdate)
   {
    if(d->TableReadFunc)
     d->StartWriteAddr = d->CurTableAddr & 0x07FFFFFF;
    else
     d->StartWriteAddr = d->CurWriteAddr & 0x07FFFFFF;
   }

   d->FinishTime = SCU_DMA_TimeCounter;
   d->Active = -1;
  }
 }
}

static void SCU_DoDMAEnd(const unsigned level)
{
 static const unsigned itab[3] = { SCU_INT_L0DMA, SCU_INT_L1DMA, SCU_INT_L2DMA };
 //printf("FIN: %08x %08x %u\n", d->CurReadBase, d->CurReadSub, d->ReadUpdate);
 DMALevel[level].Active = false;
 RecalcDMAHalt();
 SCU_SetInt(itab[level], true);
 SCU_SetInt(itab[level], false);
 CheckDMAStart(&DMALevel[level]);    
}

sscpu_timestamp_t SCU_UpdateDMA(sscpu_timestamp_t timestamp)
{
 if(timestamp < SH7095_mem_timestamp)
  return SH7095_mem_timestamp;
 //
 //
 //
 SCU_DMA_TimeCounter = std::max<int32>(std::min<int32>(SCU_DMA_RunUntil, timestamp), SCU_DMA_TimeCounter);
 SCU_DMA_RunUntil = timestamp + DMA_UpdateTimingGran;

 for(int level = 2; level >= 0; level--)
 {
  DMALevelS* d = &DMALevel[level];

  while(d->Active && SCU_DMA_TimeCounter < SCU_DMA_RunUntil)
  {
   UpdateDMAInner(d);

   if(MDFN_UNLIKELY(d->Active < 0))
   {
    if(MDFN_UNLIKELY(timestamp >= d->FinishTime))
     SCU_DoDMAEnd(level);
    else
     return d->FinishTime;
   }
  }
 }

 return SCU_DMA_RunUntil;
}

//
// Check to see if DMA is active, and if so, force the highest-priority DMA
// to finish early(kind of hacky).
//
static NO_INLINE void ForceDMAFinish(void)
{
 for(int level = 2; level >= 0; level--)
 {
  if(!DMALevel[level].Active)
   continue;

  SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Forcing hacky early DMA level %d completion.\n", level);

  if(DMALevel[level].Active > 0)
  {
   const sscpu_timestamp_t rus = SCU_DMA_RunUntil;
   //
   SCU_DMA_RunUntil = 0x7FFFFFFF;
   UpdateDMAInner(&DMALevel[level]);
   //
   SCU_DMA_RunUntil = rus;
  }

  if(DMALevel[level].Active < 0)
   SCU_DoDMAEnd(level);

  break;
 }

 SCU_DMA_TimeCounter = SCU_DMA_RunUntil;
}

static INLINE void CheckForceDMAFinish(void)
{
 if(MDFN_LIKELY(!(DMALevel[0].Active | DMALevel[1].Active | DMALevel[2].Active)))
  return;

 ForceDMAFinish();
}

//
//
//
DSPS DSP;

sscpu_timestamp_t SCU_UpdateDSP(sscpu_timestamp_t timestamp)
{
 int32 cycles = timestamp - DSP.LastTS;
 DSP.LastTS = timestamp;
 //
 //
 //
 DSP.T0_Until += cycles;	// Overflow prevented in SCU_ResetTS
 DSP.CycleCounter += cycles;
 if(DSP.CycleCounter > DSP_UpdateTimingGran)
  DSP.CycleCounter = DSP_UpdateTimingGran;

 if(MDFN_UNLIKELY(!DSP.IsRunning()))
  return SS_EVENT_DISABLED_TS;

 while(MDFN_LIKELY(DSP.CycleCounter > 0))
 {
  //printf("%02x %16llx\n", DSP.PC, DSP.NextInstr);
  ((void (*)(void))(DSP_INSTR_BASE_UIPT + (uintptr_t)(DSP_INSTR_RECOVER_TCAST)DSP.NextInstr))();
  DSP.CycleCounter -= 2;
 }

 if(MDFN_UNLIKELY(!DSP.IsRunning()))
 {
  DSP.CycleCounter += DSP_EndCCSubVal;
  return SS_EVENT_DISABLED_TS;
 }

 return timestamp + DSP_UpdateTimingGran;
}

static void DSP_Reset(bool powering_up)
{
 DSP.State = 0;
 DSP.T0_Until = 0x10000000;
 DSP.CycleCounter = 0;

 if(powering_up)
 {
  for(unsigned i = 0; i < 256; i++)
   DSP.ProgRAM[i] = DSP_DecodeInstruction(0);

  for(unsigned i = 0; i < 256; i++)
   (&DSP.DataRAM[0][0])[i] = 0;
 }

 DSP.PC = 0;
 DSP.RA = 0;
 DSP.FlagZ = false;
 DSP.FlagS = false;
 DSP.FlagV = false;
 DSP.FlagC = false;
 DSP.FlagEnd = false;
 SCU_SetInt(SCU_INT_DSP, false);

 DSP.NextInstr = DSP_DecodeInstruction(0);

 DSP.TOP = 0;
 DSP.LOP = 0;

 DSP.AC.T = 0;
 DSP.P.T = 0;

 for(unsigned i = 0; i < 4; i++)
  DSP.CT[i] = 0;

 DSP.RX = 0;
 DSP.RY = 0;

 DSP.RAO = 0;
 DSP.WAO = 0;
}

void DSP_Init(void)
{
 DSP.LastTS = 0;

 for(auto* f : DSP_GenFuncTable)
  assert((uintptr_t)f == DSP_INSTR_BASE_UIPT + ((uintptr_t)(DSP_INSTR_RECOVER_TCAST)(uint32)((uintptr_t)f - DSP_INSTR_BASE_UIPT)));

 for(auto* f : DSP_DMAFuncTable)
  assert((uintptr_t)f == DSP_INSTR_BASE_UIPT + ((uintptr_t)(DSP_INSTR_RECOVER_TCAST)(uint32)((uintptr_t)f - DSP_INSTR_BASE_UIPT)));

 for(auto* f : DSP_MVIFuncTable)
  assert((uintptr_t)f == DSP_INSTR_BASE_UIPT + ((uintptr_t)(DSP_INSTR_RECOVER_TCAST)(uint32)((uintptr_t)f - DSP_INSTR_BASE_UIPT)));

 for(auto* f : DSP_JMPFuncTable)
  assert((uintptr_t)f == DSP_INSTR_BASE_UIPT + ((uintptr_t)(DSP_INSTR_RECOVER_TCAST)(uint32)((uintptr_t)f - DSP_INSTR_BASE_UIPT)));

 for(auto* f : DSP_MiscFuncTable)
  assert((uintptr_t)f == DSP_INSTR_BASE_UIPT + ((uintptr_t)(DSP_INSTR_RECOVER_TCAST)(uint32)((uintptr_t)f - DSP_INSTR_BASE_UIPT)));
}

template<bool looped, bool hold, bool format, bool dir, unsigned drw>
static NO_INLINE NO_CLONE void DMAInstr(void)
{
 const uint32 instr = DSP_InstrPre<looped>();
 const unsigned add_mode = (instr >> 15) & 0x7;
 uint8 count;	// 0 = 256

 if(DSP.T0_Until < DSP.CycleCounter)
  DSP.CycleCounter = DSP.T0_Until &~ 1;

 DSP.T0_Until = DSP.CycleCounter;

 if(format)
 {
  const unsigned crw = instr & 0x3;
  const bool ctinc = instr & 0x4;

  count = DSP.DataRAM[crw][DSP.CT[crw]] & 0xFF;
  DSP.CT[crw] = (DSP.CT[crw] + ctinc) & 0x3F;
 }
 else
  count = instr & 0xFF;

 //printf("%02x %08x Count: %u\n", DSP.PC, instr, count);
 //SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] DSP DMA; looped=%u, add_mode=0x%01x, hold=%u, format=%u, dir=%u, drw=0x%02x -- count=%u, RAO=0x%08x, WAO=0x%08x\n", looped, add_mode, hold, format, dir, drw, count, DSP.RAO, DSP.WAO);

 if(dir)
 {
  const uint32 addr_add_amount = (1 << add_mode) &~ 1;
  uint32 addr = (DSP.WAO << 2) & 0x07FFFFFF;
  const int WriteBus = AddressToBus(addr);

  if(MDFN_UNLIKELY(WriteBus == -1))
  {
   SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Bad DSP DMA to 0x%08x --- Instr=0x%08x, Next_Instr=0x%08x, PC=0x%02x\n", addr, instr, (unsigned)(DSP.NextInstr >> 32), DSP.PC);
   return;
  }

  // Read from data RAM, write to external bus
  do
  {
   uint32 DB;

   if(drw & 0x4)
    DB = 0xFFFFFFFF;
   else
   {
    DB = DSP.DataRAM[drw][DSP.CT[drw]];
    DSP.CT[drw] = (DSP.CT[drw] + 1) & 0x3F;
   }

   if(WriteBus == 2)
   {
    ne16_wbo_be<uint32>(WorkRAMH, addr & 0xFFFFC, DB);
    addr += addr_add_amount;
    DSP.T0_Until -= 2;
   }
   else if(WriteBus == 1)
   {
    uint16 DB16;

    DB16 = DB >> 16;
    BBusRW_DB<uint16, true>(addr, &DB16, NULL, &DSP.T0_Until);

    addr += addr_add_amount;

    DB16 = DB;
    BBusRW_DB<uint16, true, true>(addr, &DB16, NULL, &DSP.T0_Until);

    addr += addr_add_amount;
   }
   else if(WriteBus == 0)
   {
    ABus_Write_DB32<uint32>(addr, DB, NULL, &DSP.T0_Until);

    addr += addr_add_amount;
   }
  } while(--count);

  if(!hold)
   DSP.WAO = (addr + 2) >> 2;
 }
 else
 {
  const uint32 addr_add_amount = (1 << (add_mode & 0x2)) &~ 1;
  uint32 addr = (DSP.RAO << 2) & 0x07FFFFFF;
  const int ReadBus = AddressToBus(addr);

  if(MDFN_UNLIKELY(ReadBus == -1))
  {
   SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] Bad DSP DMA from 0x%08x --- Instr=0x%08x, Next_Instr=0x%08x, PC=0x%02x\n", addr, instr, (unsigned)(DSP.NextInstr >> 32), DSP.PC);
   return;
  }

  if(drw & 0x4)
   SS_DBG(SS_DBG_WARNING | SS_DBG_SCU, "[SCU] DSP DMA from 0x%08x to %s --- Instr=0x%08x, Next_Instr=0x%08x, PC=0x%02x\n", addr, ((drw == 0x4) ? "program RAM" : "unknown"), instr, (unsigned)(DSP.NextInstr >> 32), DSP.PC);

  // Read from external bus, write to data RAM or program RAM
  do
  {
   uint32 DB = 0;

   if(ReadBus == 2)
   {
    DB = ne16_rbo_be<uint32>(WorkRAMH, addr & 0xFFFFF);
    DSP.T0_Until -= 2;
   }
   else if(ReadBus == 1)
   {
    uint16 tmp = 0;

    BBusRW_DB<uint16, false>(addr | 0, &tmp, NULL, &DSP.T0_Until);
    DB = tmp << 16;

    BBusRW_DB<uint16, false, true>(addr | 2, &tmp, NULL, &DSP.T0_Until);
    DB |= tmp << 0;
   }
   else if(ReadBus == 0)
    DB = ABus_Read(addr, NULL, &DSP.T0_Until);

   addr += addr_add_amount;

   if(drw & 0x4)
   {
    if(!(drw & 0x3))
    {
     DSP.ProgRAM[DSP.PC++] = DSP_DecodeInstruction(DB);
    }
   }
   else
   {
    DSP.DataRAM[drw][DSP.CT[drw]] = DB;
    DSP.CT[drw] = (DSP.CT[drw] + 1) & 0x3F;
   }
  } while(--count);

  if(!hold)
   DSP.RAO = addr >> 2;
 }
}

extern void (*const DSP_DMAFuncTable[2][8][8])(void) =
{
 #include "scu_dsp_dmatab.inc"
};

//
//
//
//
//
//
uint32 SCU_DSP_PeekProgRAM(uint8 A)
{
 return DSP.ProgRAM[A] >> 32;
}

uint32 SCU_GetRegister(const unsigned id, char* const special, const uint32 special_len)
{
 uint32 ret = 0xDEADBEEF;

 switch(id)
 {
  case SCU_GSREG_ILEVEL:
	ret = ILevel;
	break;

  case SCU_GSREG_IVEC:
	ret = IVec;
	break;

  case SCU_GSREG_IASSERTED:
	ret = IAsserted;
	break;

  case SCU_GSREG_IPENDING:
	ret = IPending;
	break;

  case SCU_GSREG_IMASK:
	ret = IMask;
	break;

  case SCU_GSREG_T0CNT:
	ret = Timer0_Counter;
	break;

  case SCU_GSREG_T0CMP:
	ret = Timer0_Compare;
	break;

  case SCU_GSREG_T0MET:
	ret = Timer0_Met;
	break;

  case SCU_GSREG_T1RLV:
	ret = Timer1_Reload;
	break;

  case SCU_GSREG_T1CNT:
	ret = Timer1_Counter;
	break;

  case SCU_GSREG_T1MOD:
	ret = Timer1_Mode;
	break;

  case SCU_GSREG_T1MET:
	ret = Timer1_Met;
	break;

  case SCU_GSREG_TENBL:
	ret = Timer_Enable;
	break;

  case SCU_GSREG_DSP_EXEC:
	ret = (bool)(DSP.State & DSPS::STATE_MASK_EXECUTE);
	break;

  case SCU_GSREG_DSP_PAUSE:
	ret = (bool)(DSP.State & DSPS::STATE_MASK_PAUSE);
	break;

  case SCU_GSREG_DSP_PC:
	ret = DSP.PC;
	break;

  case SCU_GSREG_DSP_END:
	ret = DSP.FlagEnd;
	break;
 }
 return ret;
}

void SCU_SetRegister(const unsigned id, const uint32 value)
{
 switch(id)
 {
  case SCU_GSREG_IPENDING:
	IPending = value & 0xFFFF3FFF;
	break;

  case SCU_GSREG_IMASK:
	IMask = value & 0xBFFF;
	break;
  //
  case SCU_GSREG_T0CNT:
	//Timer0_Counter = value & 0x1FF;
	break;

  case SCU_GSREG_T0CMP:
	Timer0_Compare = value & 0x3FF;
	break;

  case SCU_GSREG_T0MET:
	//Timer0_Met = value & 0x1;
	break;

  case SCU_GSREG_T1RLV:
	Timer1_Reload = value & 0x1FF;
	break;

  case SCU_GSREG_T1CNT:
	//Timer1_Counter = value & 0x1FF;
	break;

  case SCU_GSREG_T1MOD:
	Timer1_Mode = value & 0x1;
	break;

  case SCU_GSREG_T1MET:
	//Timer1_Met = value & 0x1;
	break;

  case SCU_GSREG_TENBL:
	Timer_Enable = value & 0x1;
	break;
  //

 }

 // Not quite right:
#if 0
 Timer0_Check();
 Timer1_Check();
 SCU_SetHBVB(0, HB_FromVDP2, VB_FromVDP2);
#endif

 RecalcMasterIntOut();
}

